extends Control

@export var edge_color: Color = Color(1, 1, 1, 0.75)
@export var edge_width: float = 3.0
@export var show_guides: bool = false  # 是否在Graph中绘制分隔线与标题（默认关闭，避免与面板重复）
@onready var ui_layer := get_tree().get_first_node_in_group("ui_root")
var context_btn: Button
var context_target: NodeItem = null
var info_panel: Panel

var player_pos: Vector2
var bullet_pos: Vector2

# 图标纹理
var player_texture: Texture2D
var bullet_texture: Texture2D
var energy_texture: Texture2D

# 节点仓库引用
var node_repository: NodeRepository

# 拖动相关变量
var is_dragging: bool = false
var drag_start: Vector2
var view_offset: Vector2 = Vector2.ZERO
var zoom_level: float = 1.0

# 背景图相关
var background_texture: Texture2D
var background_size: Vector2 = Vector2(3200, 2400)  # 扩大配置区域
var grid_cell_px: int = 60  # 背景网格单元（像素）
var initial_zoom: float = 0.5

# 节点数据
class NodeItem:
	var id: String
	var name: String
	var icon: Texture2D
	var position: Vector2
	var is_root: bool
	var node_type: String = "attack" # root/attack/modifier
	var size: Vector2 = Vector2(40, 40)

	func get_rect() -> Rect2:
		return Rect2(position - size * 0.5, size)

var nodes: Array[NodeItem] = []
var dragging_node: NodeItem = null
var drag_offset: Vector2 = Vector2.ZERO

# 连接相关
var edges: Array[Array] = [] # [ [from_idx:int, to_idx:int] ]
var hover_target: NodeItem = null
var can_connect_hover: bool = false
var connect_radius: float = 28.0
var connection_direction: String = "" # "up", "down", "left", "right"
var connection_offset_px: float = 120.0 # 连接后节点在屏幕像素的移动距离（与缩放无关）

# 拖动联动相关（树关系）
var node_parent_idx: Dictionary = {} # int -> int (parent index), -1 for root
var node_children_idx: Dictionary = {} # int -> Array[int]
var drag_prev_world_pos: Vector2 = Vector2.ZERO

# 攻击顺序与游标（从12点顺时针）
var _attack_order: Array[int] = []
var _attack_cursor: int = 0

var press_target: NodeItem = null
var press_pos: Vector2 = Vector2.ZERO
var did_drag: bool = false

var warehouse_items: Array[String] = []

# --- 仓库拖拽: 生成可拖出的卡片 ---
class WarehouseCard extends VBoxContainer:
	var node_id: String
	var node_name: String
	var node_icon: Texture2D
	func _ready() -> void:
		process_mode = Node.PROCESS_MODE_ALWAYS
		# 卡片自身拦截鼠标以便启动拖拽
		mouse_filter = Control.MOUSE_FILTER_STOP
		# 让子控件不夺取鼠标，保证拖拽在卡片上触发
		for c in get_children():
			if c is Control:
				(c as Control).mouse_filter = Control.MOUSE_FILTER_IGNORE
		set_process_unhandled_input(false)

	# 仅供卡片内部使用的预览构造器
	func _build_preview_local(icon: Texture2D) -> Control:
		# 根：零尺寸
		var root := Control.new()
		root.anchor_left = 0.0
		root.anchor_top = 0.0
		root.anchor_right = 0.0
		root.anchor_bottom = 0.0
		root.position = Vector2.ZERO
		root.size = Vector2.ZERO
		root.mouse_filter = Control.MOUSE_FILTER_IGNORE
		# 偏移容器：把内容移动到 (-16,-16)，使鼠标位于中心
		var offset := Control.new()
		offset.anchor_left = 0.0
		offset.anchor_top = 0.0
		offset.anchor_right = 0.0
		offset.anchor_bottom = 0.0
		offset.position = -Vector2(16, 16)
		offset.size = Vector2(32, 32)
		offset.mouse_filter = Control.MOUSE_FILTER_IGNORE
		root.add_child(offset)
		# 生成32x32位图
		var tex := TextureRect.new()
		tex.anchor_left = 0.0
		tex.anchor_top = 0.0
		tex.anchor_right = 0.0
		tex.anchor_bottom = 0.0
		tex.position = Vector2.ZERO
		tex.size = Vector2(32, 32)
		tex.custom_minimum_size = Vector2.ZERO
		tex.stretch_mode = TextureRect.STRETCH_SCALE
		tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
		var small_texture: Texture2D = icon
		if icon != null:
			var img: Image = icon.get_image()
			if img != null:
				var img2 := img.duplicate()
				img2.resize(32, 32, Image.INTERPOLATE_LANCZOS)
				small_texture = ImageTexture.create_from_image(img2)
		tex.texture = small_texture
		offset.add_child(tex)
		return root

	func _get_drag_data(_at_position):
		var data := {
			"id": node_id,
			"name": node_name,
			"icon": node_icon,
			"source_path": get_path(),
		}
		# 固定 32x32 的预览，并将其中心对准鼠标
		var preview_root := _build_preview_local(node_icon)
		set_drag_preview(preview_root)
		# 设为相对鼠标的中心偏移
		preview_root.position = -Vector2(16, 16)
		return data

	func _can_drop_data(_at_position, _data) -> bool:
		# 仓库卡片不作为放置目标
		return false

func _get_warehouse_panel() -> Control:
	var ctrl: Control = null
	# 优先：同级路径
	ctrl = get_node_or_null("../WarehouseArea") as Control
	# 备选：在 ConfigPage 下
	if ctrl == null:
		var cfg := get_node_or_null("../") as Control
		if cfg:
			ctrl = cfg.get_node_or_null("WarehouseArea") as Control
	# 备选：全树按名查找
	if ctrl == null:
		var root := get_tree().root
		var found := root.find_child("WarehouseArea", true, false)
		ctrl = found as Control
	if ctrl == null:
		print("[Warehouse] 未找到 WarehouseArea 面板！")
		return null
	# 统一强制其为可交互并优先显示
	ctrl.process_mode = Node.PROCESS_MODE_ALWAYS
	# 重要：仓库面板本体需要 STOP，才能承接并分发给子节点
	ctrl.mouse_filter = Control.MOUSE_FILTER_STOP
	ctrl.z_index = 21
	_attach_debug_input(ctrl, "WarehouseArea")
	# 同时尽量设置兄弟面板及 Graph 层级，避免覆盖
	var cfg2 := ctrl.get_parent() as Control
	if cfg2:
		var shop := cfg2.get_node_or_null("PurchaseArea") as Control
		if shop:
			shop.process_mode = Node.PROCESS_MODE_ALWAYS
			shop.mouse_filter = Control.MOUSE_FILTER_PASS
			shop.z_index = 20
		var graph := cfg2.get_node_or_null("Graph") as Control
		if graph:
			graph.z_index = 0
		var dim := cfg2.get_node_or_null("Dim") as Control
		if dim:
			dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
			dim.z_index = -50
	return ctrl

func _attach_debug_input(ctrl: Control, tag: String) -> void:
	if ctrl == null:
		return
	if bool(ctrl.get_meta("__debug_connected", false)):
		return
	ctrl.set_meta("__debug_connected", true)
	var cb := Callable(self, "_on__debug_gui_input").bind(tag)
	if not ctrl.is_connected("gui_input", cb):
		ctrl.gui_input.connect(cb)

func _on__debug_gui_input(e: InputEvent, tag: String) -> void:
	if e is InputEventMouseButton:
		var mb := e as InputEventMouseButton
		if mb.pressed:
			print("[GUI] ", tag, " 左键按下")

func _get_warehouse_list_container() -> GridContainer:
	var wh := _get_warehouse_panel()
	if wh == null:
		return null
	# 安装一个全区域输入捕捉器（PASS，不拦截，只打印）
	var catcher := wh.get_node_or_null("InputCatcher") as Control
	if catcher == null:
		catcher = Control.new()
		catcher.name = "InputCatcher"
		catcher.anchor_left = 0.0
		catcher.anchor_top = 0.0
		catcher.anchor_right = 1.0
		catcher.anchor_bottom = 1.0
		catcher.offset_left = 0
		catcher.offset_top = 0
		catcher.offset_right = 0
		# 预留底部操作区高度
		catcher.offset_bottom = -56
		catcher.mouse_filter = Control.MOUSE_FILTER_PASS
		wh.add_child(catcher)
		_attach_debug_input(catcher, "WarehouseCatcher")
	var scroll := wh.get_node_or_null("Scroll") as ScrollContainer
	if scroll == null:
		scroll = ScrollContainer.new()
		scroll.name = "Scroll"
		scroll.anchor_left = 0.0
		scroll.anchor_top = 0.0
		scroll.anchor_right = 1.0
		scroll.anchor_bottom = 1.0
		scroll.offset_left = 6
		scroll.offset_top = 6
		scroll.offset_right = -6
		scroll.offset_bottom = -62
		scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
		# 让事件先给子控件，避免被滚动容器吃掉
		scroll.mouse_filter = Control.MOUSE_FILTER_PASS
		wh.add_child(scroll)
	# 确保暂停时也处理输入，并附加调试
	scroll.process_mode = Node.PROCESS_MODE_ALWAYS
	_attach_debug_input(scroll, "ScrollContainer")
	var grid := scroll.get_node_or_null("ItemsGrid") as GridContainer
	if grid == null:
		grid = GridContainer.new()
		grid.name = "ItemsGrid"
		grid.columns = 3
		grid.custom_minimum_size = Vector2(0, 0)
		grid.add_theme_constant_override("h_separation", 8)
		grid.add_theme_constant_override("v_separation", 8)
		# 让网格本身不拦截，事件交给卡片
		grid.mouse_filter = Control.MOUSE_FILTER_PASS
		scroll.add_child(grid)
	# 确保暂停时也处理输入，并附加调试
	grid.process_mode = Node.PROCESS_MODE_ALWAYS
	_attach_debug_input(grid, "ItemsGrid")
	# 底部操作区：刷新/下一关
	var action_bar := wh.get_node_or_null("ActionBar") as HBoxContainer
	if action_bar == null:
		action_bar = HBoxContainer.new()
		action_bar.name = "ActionBar"
		action_bar.anchor_left = 0.0
		action_bar.anchor_top = 1.0
		action_bar.anchor_right = 1.0
		action_bar.anchor_bottom = 1.0
		action_bar.offset_left = 6
		action_bar.offset_right = -6
		action_bar.offset_top = -50
		action_bar.offset_bottom = -6
		action_bar.add_theme_constant_override("separation", 12)
		action_bar.mouse_filter = Control.MOUSE_FILTER_STOP
		wh.add_child(action_bar)
		# 刷新商店按钮
		var btn_refresh := Button.new()
		btn_refresh.name = "BtnRefreshShop"
		btn_refresh.text = "刷新商店"
		btn_refresh.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		btn_refresh.pressed.connect(_on_refresh_shop_pressed)
		action_bar.add_child(btn_refresh)
		# 下一关按钮
		var btn_next := Button.new()
		btn_next.name = "BtnNextStage"
		btn_next.text = "下一关"
		btn_next.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		btn_next.pressed.connect(_on_next_stage_pressed)
		action_bar.add_child(btn_next)
	return grid

func _append_warehouse_entry(n: NodeItem) -> void:
	var grid := _get_warehouse_list_container()
	if grid == null:
		return
	# 单元卡片（可拖拽）
	var card := WarehouseCard.new()
	card.node_id = n.id
	card.node_name = n.name
	card.node_icon = n.icon
	card.custom_minimum_size = Vector2(72, 92)
	card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	grid.add_child(card)
	# 图标
	var icon := TextureRect.new()
	icon.texture = n.icon
	icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
	icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	icon.custom_minimum_size = Vector2(64, 64)
	icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
	card.add_child(icon)
	# 名称
	var lbl := Label.new()
	lbl.text = n.name
	lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	card.add_child(lbl)
	# 记录到 meta，便于通用绑定
	card.set_meta("node_id", n.id)
	card.set_meta("node_name", n.name)
	card.set_meta("node_icon", n.icon)
	# 为新卡片做通用拖拽绑定（即使不是 WarehouseCard 也适用）
	_setup_generic_card_interaction(card)
	warehouse_items.append(n.id)

# 遍历已有卡片（包括非 WarehouseCard）并绑定拖拽
func _ensure_warehouse_cards_interactive() -> void:
	var grid := _get_warehouse_list_container()
	if grid == null:
		return
	for c in grid.get_children():
		if c is Control:
			_setup_generic_card_interaction(c as Control)

func _setup_generic_card_interaction(card: Control) -> void:
	if not is_instance_valid(card):
		return
	# 如果本身就是 WarehouseCard，则交给类内置的拖拽实现，避免重复绑定
	if card is WarehouseCard:
		return
	# 设置鼠标行为，保证事件留在卡片本体
	card.mouse_filter = Control.MOUSE_FILTER_STOP
	for cc in card.get_children():
		if cc is Control:
			(cc as Control).mouse_filter = Control.MOUSE_FILTER_IGNORE
	# 若已连接则跳过（使用 meta 标记去重）
	if bool(card.get_meta("__card_connected", false)):
		return
	card.set_meta("__card_connected", true)
	# 解析节点信息（从 meta 或子节点推断）
	var node_id := String(card.get_meta("node_id", ""))
	var node_name := String(card.get_meta("node_name", ""))
	var node_icon := card.get_meta("node_icon") as Texture2D
	if node_name == "":
		for cc in card.get_children():
			if cc is Label:
				node_name = (cc as Label).text
				break
	if node_icon == null:
		for cc in card.get_children():
			if cc is TextureRect:
				node_icon = (cc as TextureRect).texture
				break
	if node_id == "" and node_name != "":
		# 简单映射：基础子弹 -> basic_bullet
		if node_name.find("基础子弹") != -1:
			node_id = "basic_bullet"
		else:
			node_id = node_name
	# 将节点数据写入 meta，handler 从 meta 读取
	card.set_meta("__node_id", node_id)
	card.set_meta("__node_name", node_name)
	card.set_meta("__node_icon", node_icon)
	# 使用 Callable.bind，匹配 gui_input(event) 签名
	var cb := Callable(self, "_on_card_gui_input").bind(card)
	if not card.is_connected("gui_input", cb):
		card.gui_input.connect(cb)
	print("[Warehouse] 绑定拖拽: ", node_name)

func _on_card_gui_input(event: InputEvent, card: Control) -> void:
	var node_id := String(card.get_meta("__node_id", ""))
	var node_name := String(card.get_meta("__node_name", ""))
	var node_icon := card.get_meta("__node_icon") as Texture2D
	if event is InputEventMouseButton:
		var mb := event as InputEventMouseButton
		if mb.button_index == MOUSE_BUTTON_LEFT:
			if mb.pressed:
				print("[Warehouse] 按下卡片: ", node_name)
				card.set_meta("_pressing", true)
				card.set_meta("_press_pos", mb.position)
				card.set_meta("_drag_started", false)
				accept_event()
			else:
				card.set_meta("_pressing", false)
				card.set_meta("_drag_started", false)
	elif event is InputEventMouseMotion:
		var pressing := bool(card.get_meta("_pressing", false))
		var started := bool(card.get_meta("_drag_started", false))
		if pressing and not started:
			var press_pos_local := card.get_meta("_press_pos", Vector2.ZERO) as Vector2
			var mm := event as InputEventMouseMotion
			if (mm.position - press_pos_local).length() > 4.0:
				var data := {
					"id": node_id,
					"name": node_name,
					"icon": node_icon,
					"source_path": card.get_path(),
				}
				var preview_root := Control.new()
				preview_root.anchor_left = 0.0
				preview_root.anchor_top = 0.0
				preview_root.anchor_right = 0.0
				preview_root.anchor_bottom = 0.0
				preview_root.position = -Vector2(16, 16)
				preview_root.size = Vector2(32, 32)
				preview_root.custom_minimum_size = Vector2(32, 32)
				preview_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
				var tex := TextureRect.new()
				tex.anchor_left = 0.0
				tex.anchor_top = 0.0
				tex.anchor_right = 0.0
				tex.anchor_bottom = 0.0
				tex.position = Vector2.ZERO
				tex.size = Vector2(32, 32)
				tex.custom_minimum_size = Vector2.ZERO
				tex.stretch_mode = TextureRect.STRETCH_SCALE
				tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
				var small_texture: Texture2D = node_icon
				if node_icon != null:
					var img: Image = node_icon.get_image()
					if img != null:
						var img2 := img.duplicate()
						img2.resize(32, 32, Image.INTERPOLATE_LANCZOS)
						small_texture = ImageTexture.create_from_image(img2)
				tex.texture = small_texture
				preview_root.add_child(tex)
				card.force_drag(data, preview_root)
				card.set_meta("_drag_started", true)
				print("[Warehouse] 启动拖拽 -> ", get_viewport().gui_is_dragging())
				accept_event()

# --- Graph 接收拖拽放置 ---
func _can_drop_data(_at_position, data) -> bool:
	return typeof(data) == TYPE_DICTIONARY and data.has("id") and data.has("icon") and data.has("name")

func _drop_data(at_position, data) -> void:
	if not _can_drop_data(at_position, data):
		return
	# 将屏幕坐标转换为世界坐标（背景坐标）
	var screen_center = get_rect().size * 0.5
	var scaled_size = background_size * zoom_level
	var bg_pos = screen_center - scaled_size * 0.5 + view_offset
	var world: Vector2 = (at_position - bg_pos) / zoom_level
	# 创建新节点
	var ni := NodeItem.new()
	ni.id = String(data["id"])
	ni.name = String(data["name"])
	ni.icon = data["icon"] as Texture2D
	ni.position = world
	ni.is_root = false
	
	# 从节点仓库获取正确的节点类型
	if node_repository:
		var node_data = node_repository.get_node_by_id(ni.id)
		if node_data:
			ni.node_type = node_data.node_type
		else:
			ni.node_type = "attack"  # 默认类型
	else:
		ni.node_type = "attack"  # 默认类型
	
	nodes.append(ni)
	queue_redraw()
	# 从仓库中移除对应卡片
	if data.has("source_path"):
		_remove_warehouse_card_by_path(String(data["source_path"]))

func _remove_warehouse_card_by_path(path_str: String) -> void:
	if path_str == "":
		return
	var node := get_node_or_null(path_str)
	if node == null:
		return
	if node is Control:
		# 更新内部列表
		var id := ""
		if node.has_meta("node_id"):
			id = String(node.get_meta("node_id"))
		elif node is WarehouseCard:
			id = (node as WarehouseCard).node_id
		if id != "":
			for i in range(warehouse_items.size()):
				if warehouse_items[i] == id:
					warehouse_items.remove_at(i)
					break
		node.queue_free()

func _ready() -> void:
	set_process(false)
	# 在暂停状态下也继续接收输入（Godot 4 使用 process_mode）
	process_mode = Node.PROCESS_MODE_ALWAYS
	# 图层降低：在商店/仓库下面显示
	z_index = 0
	# 拦截鼠标，直接由 Graph 处理（避免被遮挡控件拦截）
	mouse_filter = Control.MOUSE_FILTER_STOP
	# 开启输入处理
	set_process_input(true)
	set_process_unhandled_input(true)
	_load_textures()
	_create_background()
	_update_layout()
	_build_initial_nodes()
	print("[ConfigGraph] ready, visible=", visible)
	# 立即布线配置页，附加输入日志与鼠标策略
	_wire_config_page()
	# 延迟一次，遍历并绑定已有仓库卡片（兼容旧结构）
	call_deferred("_ensure_warehouse_cards_interactive")
	# 初始化仓库物品
	call_deferred("_populate_warehouse_from_repository")
	queue_redraw()

func _wire_config_page() -> void:
	var cfg := get_parent() as Control
	if cfg == null:
		return
	_attach_debug_input(cfg, "ConfigPage")
	# 主区域设置
	var dim := cfg.get_node_or_null("Dim") as Control
	if dim:
		dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
		dim.z_index = -50
		_attach_debug_input(dim, "Dim")
	var shop := cfg.get_node_or_null("PurchaseArea") as Control
	if shop:
		shop.mouse_filter = Control.MOUSE_FILTER_PASS
		shop.z_index = 20
		_attach_debug_input(shop, "PurchaseArea")
	var wh := cfg.get_node_or_null("WarehouseArea") as Control
	if wh:
		# 重要：仓库面板使用 STOP 接管事件
		wh.mouse_filter = Control.MOUSE_FILTER_STOP
		wh.z_index = 21
		_attach_debug_input(wh, "WarehouseArea")
		# 确保其内部的 Scroll/Grid 存在并挂载调试
		var _grid := _get_warehouse_list_container()
	var g := cfg.get_node_or_null("Graph") as Control
	if g:
		g.z_index = 0
		_attach_debug_input(g, "GraphContainer")

func _create_background() -> void:
	# 创建一个简单的网格背景纹理
	var image = Image.create(int(background_size.x), int(background_size.y), false, Image.FORMAT_RGBA8)
	image.fill(Color(0.1, 0.1, 0.15, 1.0))  # 深蓝灰色背景

	# 绘制网格线
	var grid_size = grid_cell_px
	var grid_color = Color(0.22, 0.22, 0.28, 0.85)
	
	# 垂直线
	for x in range(0, int(background_size.x), grid_size):
		for y in range(int(background_size.y)):
			image.set_pixel(x, y, grid_color)
	
	# 水平线
	for y in range(0, int(background_size.y), grid_size):
		for x in range(int(background_size.x)):
			image.set_pixel(x, y, grid_color)
	
	# 中心十字线
	var center_x = int(background_size.x / 2)
	var center_y = int(background_size.y / 2)
	var center_color = Color(0.4, 0.4, 0.6, 1.0)
	
	for x in range(int(background_size.x)):
		image.set_pixel(x, center_y, center_color)
	for y in range(int(background_size.y)):
		image.set_pixel(center_x, y, center_color)
	
	background_texture = ImageTexture.create_from_image(image)
	# 设置初始缩放
	zoom_level = initial_zoom

func _load_textures() -> void:
	# 加载玩家图标
	var player_path = "res://imageico/Root.png"
	if ResourceLoader.exists(player_path):
		player_texture = load(player_path)
	else:
		print("警告：找不到玩家图标文件：", player_path)
	
	# 从节点仓库获取基础子弹节点信息
	node_repository = get_tree().get_first_node_in_group("node_repository")
	if not node_repository:
		# 如果没有找到分组，直接查找节点
		node_repository = get_tree().get_node_or_null("NodeRepository")
	
	if node_repository:
		var basic_bullet_node = node_repository.get_node_by_id("basic_bullet")
		if basic_bullet_node:
			var bullet_path = basic_bullet_node.icon_path
			if ResourceLoader.exists(bullet_path):
				bullet_texture = load(bullet_path)
				print("从节点仓库加载基础子弹图标：", bullet_path)
			else:
				print("警告：找不到基础子弹图标文件：", bullet_path)
		else:
			print("警告：节点仓库中找不到基础子弹节点")
	else:
		print("警告：找不到节点仓库")
		# 备用方案：直接加载图标
		var bullet_path = "res://imageico/bullet_effective.png"
		if ResourceLoader.exists(bullet_path):
			bullet_texture = load(bullet_path)
	
	# 加载能量图标
	var energy_path = "res://imageico/energy_effective.png"
	if ResourceLoader.exists(energy_path):
		energy_texture = load(energy_path)
		print("加载能量图标：", energy_path)
	else:
		print("警告：找不到能量图标文件：", energy_path)

# 构建固定大小并可居中的拖拽预览
func _build_drag_preview(icon: Texture2D) -> Control:
	var root := Control.new()
	root.custom_minimum_size = Vector2(64, 64)
	root.mouse_filter = Control.MOUSE_FILTER_IGNORE
	# 图片子控件（固定尺寸，不随父级拉伸）
	var tex := TextureRect.new()
	tex.texture = icon
	tex.anchor_left = 0.0
	tex.anchor_top = 0.0
	tex.anchor_right = 0.0
	tex.anchor_bottom = 0.0
	tex.position = Vector2.ZERO
	tex.size = Vector2(64, 64)
	tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
	root.add_child(tex)
	# 以中心对齐到鼠标
	root.position = -root.custom_minimum_size * 0.5
	return root

# 将拖拽预览在被系统接管后延迟居中到鼠标
func _center_drag_preview_deferred(preview: Control) -> void:
	if not is_instance_valid(preview):
		return
	# 以固定 64x64 为基准居中
	preview.position = -Vector2(32, 32)

# 直接在 gui_input 中处理，确保优先级最高
func _gui_input(event: InputEvent) -> void:
	_handle_any_input(event)

# 将未被其它控件消费的输入统一转发到交互处理
func _unhandled_input(event: InputEvent) -> void:
	_handle_any_input(event)

var is_panning: bool = false
var pan_anchor: Vector2 = Vector2.ZERO

# --- Graph 接收输入的统一入口 ---
func _handle_any_input(event: InputEvent) -> void:
	if not visible:
		return
	# 若UI正处于拖拽流程（例如从仓库卡片拖出），图层不响应任何平移/选中/拖节点
	if get_viewport().gui_is_dragging():
		return
	# 鼠标在阻挡区域（仓库/商店）上方时，不处理图层交互，避免误触
	if event is InputEventMouse:
		var m := event as InputEventMouse
		if _is_over_ui_blocker(m.position):
			return
	var within := false
	if event is InputEventMouse:
		var xform_inv: Transform2D = get_global_transform_with_canvas().affine_inverse()
		var local_pos: Vector2 = xform_inv * (event as InputEventMouse).position
		within = Rect2(Vector2.ZERO, get_rect().size).has_point(local_pos)
	else:
		within = true
	if not within:
		return
	var screen_center = get_rect().size * 0.5
	var scaled_size = background_size * zoom_level
	var background_pos = screen_center - scaled_size * 0.5 + view_offset
	if event is InputEventMouseButton:
		var mb := event as InputEventMouseButton
		# 滚轮：缩放
		if mb.button_index == MOUSE_BUTTON_WHEEL_UP:
			_zoom_in(mb.position)
			_hide_context_btn()
			return
		elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			_zoom_out(mb.position)
			_hide_context_btn()
			return
		# 中键或右键：背景平移
		if (mb.button_index == MOUSE_BUTTON_MIDDLE or mb.button_index == MOUSE_BUTTON_RIGHT):
			if mb.pressed:
				is_panning = true
				pan_anchor = mb.position - view_offset
			else:
				is_panning = false
			return
		# 左键：节点命中与拖拽/点击/空白处平移
		if mb.button_index == MOUSE_BUTTON_LEFT:
			if mb.pressed:
				var hit: NodeItem = null
				for i in range(nodes.size()-1, -1, -1):
					var n := nodes[i]
					var pos: Vector2 = background_pos + n.position * zoom_level
					var rect: Rect2 = Rect2(pos - n.size * zoom_level * 0.5, n.size * zoom_level)
					if rect.has_point(mb.position):
						hit = n
						break
				if hit != null and not hit.is_root:
					press_target = hit
					press_pos = mb.position
					did_drag = false
					dragging_node = hit
					drag_offset = (mb.position - (background_pos + hit.position * zoom_level)) / zoom_level
					drag_prev_world_pos = hit.position
					# 清理拖拽状态
					hover_target = null
					can_connect_hover = false
					print("[ConfigGraph] 开始拖动节点:", hit.name)
				elif hit != null and hit.is_root:
					print("[ConfigGraph] 根节点不可拖动")
				else:
					# 左键在空白处 -> 开启平移
					is_panning = true
					pan_anchor = mb.position - view_offset
			else:
				# 左键释放
				if dragging_node != null:
					print("[ConfigGraph] 结束拖动节点:", dragging_node.name)
					# 释放时若存在可连接目标且满足规则，则添加连接
					if hover_target != null and _can_connect(dragging_node, hover_target):
						_make_connection(dragging_node, hover_target)
						hover_target = null
						can_connect_hover = false
					dragging_node = null
					drag_prev_world_pos = Vector2.ZERO
				# 如果按下在节点上且没有明显拖动，则视为点击 -> 显示按钮
				if press_target != null and not press_target.is_root and not did_drag:
					_show_context_btn(press_target)
				else:
					_hide_context_btn()
				press_target = null
				did_drag = false
				# 不论命中与否，松开左键均结束平移
				is_panning = false
	elif event is InputEventMouseMotion:
		var mm := event as InputEventMouseMotion
		# 正在平移背景
		if is_panning:
			view_offset = mm.position - pan_anchor
			queue_redraw()
			return
		# 正在拖拽节点
		if dragging_node != null:
			var new_world: Vector2 = (mm.position - background_pos) / zoom_level
			var new_pos := new_world - drag_offset
			var delta := new_pos - drag_prev_world_pos
			dragging_node.position = new_pos
			if delta != Vector2.ZERO:
				_move_descendants(dragging_node, delta)
				drag_prev_world_pos = new_pos
			# 更新连接提示
			hover_target = _find_connect_target(mm.position, background_pos)
			can_connect_hover = _can_connect(dragging_node, hover_target)
			queue_redraw()
			_hide_context_btn()
			# 判断是否产生了拖拽
			if press_target != null and (mm.position - press_pos).length() > 6.0:
				did_drag = true

func _is_over_ui_blocker(global_pos: Vector2) -> bool:
	var cfg := get_node_or_null("../ConfigPage") as Control
	if cfg == null:
		# 我们自身在 ConfigPage 下，但兜底
		cfg = get_parent() as Control
	var wh := cfg.get_node_or_null("WarehouseArea") as Control
	var shop := cfg.get_node_or_null("PurchaseArea") as Control
	return _point_in_control(global_pos, wh) or _point_in_control(global_pos, shop)

func _point_in_control(global_pos: Vector2, ctrl: Control) -> bool:
	if ctrl == null or not ctrl.is_visible_in_tree():
		return false
	var xform_inv: Transform2D = ctrl.get_global_transform_with_canvas().affine_inverse()
	var local_pos: Vector2 = xform_inv * global_pos
	return Rect2(Vector2.ZERO, ctrl.get_rect().size).has_point(local_pos)

func _start_drag(pos: Vector2) -> void:
	is_dragging = true
	drag_start = pos - view_offset

func _end_drag() -> void:
	is_dragging = false

func _update_drag(pos: Vector2) -> void:
	view_offset = pos - drag_start
	queue_redraw()

func _zoom_in(mouse_pos: Vector2) -> void:
	var old_zoom = zoom_level
	var screen_center = get_rect().size * 0.5
	var scaled_size_before = background_size * zoom_level
	var bg_pos_before = screen_center - scaled_size_before * 0.5 + view_offset
	# 鼠标所在的世界坐标（以背景为世界）
	var world_pos = (mouse_pos - bg_pos_before) / old_zoom
	zoom_level = min(zoom_level * 1.1, 5.0)
	var scaled_size_after = background_size * zoom_level
	var desired_bg_pos = mouse_pos - world_pos * zoom_level
	view_offset = desired_bg_pos - (screen_center - scaled_size_after * 0.5)
	queue_redraw()

func _zoom_out(mouse_pos: Vector2) -> void:
	var old_zoom = zoom_level
	var screen_center = get_rect().size * 0.5
	var scaled_size_before = background_size * zoom_level
	var bg_pos_before = screen_center - scaled_size_before * 0.5 + view_offset
	# 鼠标所在的世界坐标（以背景为世界）
	var world_pos = (mouse_pos - bg_pos_before) / old_zoom
	zoom_level = max(zoom_level / 1.1, 1.0)
	var scaled_size_after = background_size * zoom_level
	var desired_bg_pos = mouse_pos - world_pos * zoom_level
	view_offset = desired_bg_pos - (screen_center - scaled_size_after * 0.5)
	queue_redraw()

func _notification(what: int) -> void:
	if what == NOTIFICATION_RESIZED:
		_update_layout()
		queue_redraw()

func _update_layout() -> void:
	var _r := get_rect()
	# 将初始节点放在背景中心附近，并缩小彼此间距
	var center := background_size * 0.5
	var half_gap := 120.0  # 两节点中心间距的一半，可按需微调
	player_pos = center + Vector2(-half_gap, 0)
	bullet_pos = center + Vector2(half_gap, 0)

func _draw() -> void:
	var _r := get_rect()
	
	# 可选：绘制区域分隔线与标题（默认关闭）
	if show_guides:
		_draw_area_separators()
		_draw_area_titles()
	
	# 绘制背景图（应用缩放和偏移）
	_draw_background()
	
	# 绘制节点配置图（应用缩放和偏移）
	_draw_node_graph()

func _draw_background() -> void:
	if not background_texture:
		return
	
	# 计算背景图在屏幕上的位置和大小
	var screen_center = get_rect().size * 0.5
	var scaled_size = background_size * zoom_level
	var background_pos = screen_center - scaled_size * 0.5 + view_offset
	
	# 绘制背景图
	var rect = Rect2(background_pos, scaled_size)
	draw_texture_rect(background_texture, rect, false)
	
	# 绘制背景边界框（调试用）
	draw_rect(rect, Color(1, 1, 0, 0.3), false, 2.0)

func _draw_node_graph() -> void:
	if not background_texture:
		return
	var screen_center = get_rect().size * 0.5
	var scaled_size = background_size * zoom_level
	var background_pos = screen_center - scaled_size * 0.5 + view_offset
	# 画连线
	for e in edges:
		if e.size() != 2:
			continue
		var i0: int = int(e[0])
		var i1: int = int(e[1])
		if i0 < 0 or i0 >= nodes.size() or i1 < 0 or i1 >= nodes.size():
			continue
		var p0: Vector2 = background_pos + nodes[i0].position * zoom_level
		var p1: Vector2 = background_pos + nodes[i1].position * zoom_level
		draw_line(p0, p1, edge_color, edge_width)
	# 画节点
	for n in nodes:
		var pos: Vector2 = background_pos + n.position * zoom_level
		var icon_size: Vector2 = n.size * zoom_level
		var rect: Rect2 = Rect2(pos - icon_size * 0.5, icon_size)
		if n.icon:
			draw_texture_rect(n.icon, rect, false)
		# 名称
		_draw_centered_label(n.name, pos, Color(1,1,1,0.95))
		
		# 能量显示
		var energy_cost := _calculate_path_energy_cost(n)
		var energy_text := ""
		var energy_color := Color(0, 1, 0, 0.9)  # 默认绿色
		
		if n.node_type == "root":
			energy_text = "[" + str(energy_cost) + "]"
			energy_color = Color(0, 0.5, 1, 0.9)  # 蓝色
			# 在根节点能量文本左侧绘制能量图标
			if energy_texture:
				var energy_icon_size := Vector2(16, 16) * zoom_level
				var energy_icon_pos := pos + Vector2(-25 * zoom_level, icon_size.y * 0.5 + 15 * zoom_level) - energy_icon_size * 0.5
				var energy_icon_rect := Rect2(energy_icon_pos, energy_icon_size)
				draw_texture_rect(energy_texture, energy_icon_rect, false)
		else:
			energy_text = "[" + str(energy_cost) + "]"
			energy_color = Color(0, 1, 0, 0.9)  # 绿色
		
		# 在节点下方显示能量
		var energy_pos := pos + Vector2(0, icon_size.y * 0.5 + 8 * zoom_level)
		_draw_centered_label(energy_text, energy_pos, energy_color)
	# 连接提示
	if hover_target and can_connect_hover:
		var pos_h: Vector2 = background_pos + hover_target.position * zoom_level
		_draw_connection_hint(pos_h)

func _draw_player_icon(c: Vector2) -> void:
	if player_texture:
		# 绘制玩家图标，调整到合适大小
		var icon_size = Vector2(40, 40) * zoom_level  # 应用缩放
		var rect = Rect2(c - icon_size * 0.5, icon_size)
		draw_texture_rect(player_texture, rect, false)

func _draw_bullet_icon(c: Vector2) -> void:
	if bullet_texture:
		# 绘制基础子弹图标，保持相同大小
		var icon_size = Vector2(40, 40) * zoom_level  # 应用缩放
		var rect = Rect2(c - icon_size * 0.5, icon_size)
		draw_texture_rect(bullet_texture, rect, false)

func _draw_centered_label(text: String, center: Vector2, col: Color) -> void:
	var fnt: Font = get_theme_default_font()
	if fnt == null:
		return
	var font_size := int(18 * zoom_level)  # 应用缩放
	var text_size = fnt.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
	var pos: Vector2 = center - text_size * 0.5 + Vector2(0, 26 * zoom_level)
	draw_string(fnt, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, col)

func _rebuild_tree_relations() -> void:
	# 以根为起点构建父子关系（简单 BFS）
	node_parent_idx.clear()
	node_children_idx.clear()
	# 找到根节点索引（第一个 is_root）
	var root_idx := -1
	for i in nodes.size():
		if nodes[i].is_root:
			root_idx = i
			break
	if root_idx == -1:
		return
	# 初始化结构
	for i in nodes.size():
		node_children_idx[i] = []
		node_parent_idx[i] = -2 # 未访问标记
	# 从根开始
	var q: Array[int] = [root_idx]
	node_parent_idx[root_idx] = -1
	while q.size() > 0:
		var cur: int = q.pop_front()
		# 遍历所有边，寻找 cur 的邻接点
		for e in edges:
			var a: int = int(e[0])
			var b: int = int(e[1])
			var nxt: int = -1
			if a == cur:
				nxt = b
			elif b == cur:
				nxt = a
			if nxt != -1 and node_parent_idx[nxt] == -2:
				node_parent_idx[nxt] = cur
				(node_children_idx[cur] as Array).append(nxt)
				q.append(nxt)

func _collect_descendants_indices(idx: int) -> Array[int]:
	var result: Array[int] = []
	var stack: Array[int] = []
	if node_children_idx.has(idx):
		for c in node_children_idx[idx]:
			stack.append(int(c))
	while stack.size() > 0:
		var cur: int = stack.pop_back()
		result.append(cur)
		if node_children_idx.has(cur):
			for c in node_children_idx[cur]:
				stack.append(int(c))
	return result

func _move_descendants(source_node: NodeItem, delta_world: Vector2) -> void:
	# 将 delta 应用到 source_node 的所有后代
	var src_idx: int = nodes.find(source_node)
	if src_idx == -1:
		return
	var list: Array[int] = _collect_descendants_indices(src_idx)
	for i in list:
		if i >= 0 and i < nodes.size():
			nodes[i].position += delta_world

func _draw_connection_hint(center: Vector2) -> void:
	# 绘制连接提示圆圈
	draw_circle(center, connect_radius * zoom_level, Color(0.2, 1.0, 0.2, 0.35))
	draw_circle(center, (connect_radius+4.0) * zoom_level, Color(0.2, 1.0, 0.2, 0.6))
	
	# 绘制方向箭头
	var arrow_size := 12.0 * zoom_level
	var arrow_offset := (connect_radius + 8.0) * zoom_level
	
	match connection_direction:
		"up":
			var arrow_pos := center + Vector2(0, -arrow_offset)
			_draw_arrow(arrow_pos, Vector2(0, -1), arrow_size, Color(0.2, 1.0, 0.2, 0.8))
		"down":
			var arrow_pos := center + Vector2(0, arrow_offset)
			_draw_arrow(arrow_pos, Vector2(0, 1), arrow_size, Color(0.2, 1.0, 0.2, 0.8))
		"left":
			var arrow_pos := center + Vector2(-arrow_offset, 0)
			_draw_arrow(arrow_pos, Vector2(-1, 0), arrow_size, Color(0.2, 1.0, 0.2, 0.8))
		"right":
			var arrow_pos := center + Vector2(arrow_offset, 0)
			_draw_arrow(arrow_pos, Vector2(1, 0), arrow_size, Color(0.2, 1.0, 0.2, 0.8))

func _draw_arrow(pos: Vector2, direction: Vector2, arrow_size: float, color: Color) -> void:
	# 绘制简单的三角形箭头
	var points := PackedVector2Array()
	var half_size := arrow_size * 0.5
	
	# 根据方向计算箭头顶点
	if direction.y < 0: # 向上
		points.append(pos + Vector2(0, half_size))
		points.append(pos + Vector2(-half_size, -half_size))
		points.append(pos + Vector2(half_size, -half_size))
	elif direction.y > 0: # 向下
		points.append(pos + Vector2(0, -half_size))
		points.append(pos + Vector2(-half_size, half_size))
		points.append(pos + Vector2(half_size, half_size))
	elif direction.x < 0: # 向左
		points.append(pos + Vector2(half_size, 0))
		points.append(pos + Vector2(-half_size, -half_size))
		points.append(pos + Vector2(-half_size, half_size))
	else: # 向右
		points.append(pos + Vector2(-half_size, 0))
		points.append(pos + Vector2(half_size, -half_size))
		points.append(pos + Vector2(half_size, half_size))
	
	draw_colored_polygon(points, color)

func _adjust_node_position(dragged_node: NodeItem, target_node: NodeItem, direction: String) -> void:
	# 根据连接方向调整被拖拽节点的位置
	# 距离 = (两个节点在该轴的半尺寸和) + 屏幕像素边距(与缩放无关)
	# 目标期望：任何缩放倍率下为 1.5 个背景格的视觉距离
	var desired_cells := 1.5
	# 网格在屏幕上的像素大小 = grid_cell_px * zoom_level
	var desired_px := desired_cells * float(grid_cell_px) * zoom_level
	# 用像素计算后转换为世界单位，确保屏幕上始终为 1.5 格
	var margin_world := desired_px / zoom_level
	var half_w_sum := (target_node.size.x * 0.5) + (dragged_node.size.x * 0.5)
	var half_h_sum := (target_node.size.y * 0.5) + (dragged_node.size.y * 0.5)
	
	match direction:
		"up":
			dragged_node.position = target_node.position + Vector2(0, -(half_h_sum + margin_world))
		"down":
			dragged_node.position = target_node.position + Vector2(0, (half_h_sum + margin_world))
		"left":
			dragged_node.position = target_node.position + Vector2(-(half_w_sum + margin_world), 0)
		"right":
			dragged_node.position = target_node.position + Vector2((half_w_sum + margin_world), 0)
		_:
			# 默认情况，保持当前位置
			pass

func _draw_area_separators() -> void:
	var _r := get_rect()
	
	# 水平分隔线：将界面分为上下两部分（节点配置区域在上方）
	var horizontal_y = _r.size.y * 0.75
	draw_line(Vector2(0, horizontal_y), Vector2(_r.size.x, horizontal_y), Color(1, 1, 1, 0.3), 2.0)
	
	# 垂直分隔线：将右侧分为仓库区域
	var vertical_x = _r.size.x * 0.75
	draw_line(Vector2(vertical_x, 0), Vector2(vertical_x, horizontal_y), Color(1, 1, 1, 0.3), 2.0)

func _draw_area_titles() -> void:
	var _r := get_rect()
	var fnt: Font = get_theme_default_font()
	if fnt == null:
		return
	
	var font_size := 20
	var title_color = Color(1, 1, 1, 0.9)
	
	# 节点配置区域标题（左上角）
	var config_title = "节点配置区域"
	var config_pos = Vector2(_r.size.x * 0.15, _r.size.y * 0.05)
	var _config_text_size = fnt.get_string_size(config_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
	draw_string(fnt, config_pos, config_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, title_color)
	
	# 商店购买区域标题（下方）
	var shop_title = "商店区域"
	var shop_pos = Vector2(_r.size.x * 0.5, _r.size.y * 0.8)
	var shop_text_size = fnt.get_string_size(shop_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
	var shop_text_pos: Vector2 = shop_pos - shop_text_size * 0.5
	draw_string(fnt, shop_text_pos, shop_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, title_color)
	
	# 玩家仓库区域标题（右侧）
	var warehouse_title = "玩家仓库"
	var warehouse_pos = Vector2(_r.size.x * 0.875, _r.size.y * 0.375)
	var warehouse_text_size = fnt.get_string_size(warehouse_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
	var warehouse_text_pos: Vector2 = warehouse_pos - warehouse_text_size * 0.5
	draw_string(fnt, warehouse_text_pos, warehouse_title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, title_color)

func _build_initial_nodes() -> void:
	nodes.clear()
	# 只创建根节点，不自动创建攻击节点
	var root := NodeItem.new()
	root.id = "root"
	root.name = "玩家"
	root.icon = player_texture
	root.position = player_pos
	root.is_root = true
	root.node_type = "root"
	nodes.append(root)
	edges.clear()

func _ensure_context_btn():
	if context_btn and is_instance_valid(context_btn):
		return
	context_btn = Button.new()
	context_btn.text = "放回仓库"
	context_btn.z_index = 200
	context_btn.visible = false
	context_btn.mouse_filter = Control.MOUSE_FILTER_STOP
	add_child(context_btn)
	context_btn.pressed.connect(_on_context_return_pressed)

func _ensure_info_panel():
	if info_panel and is_instance_valid(info_panel):
		return
	info_panel = Panel.new()
	info_panel.name = "NodeInfoPanel"
	info_panel.z_index = 300
	info_panel.visible = false
	info_panel.mouse_filter = Control.MOUSE_FILTER_STOP
	info_panel.custom_minimum_size = Vector2(260, 0)
	# 半透明背景 + 边框（更明显）
	var sb := StyleBoxFlat.new()
	sb.bg_color = Color(0, 0, 0, 0.6)
	sb.border_color = Color(1, 1, 1, 0.8)
	sb.border_width_top = 2
	sb.border_width_bottom = 2
	sb.border_width_left = 2
	sb.border_width_right = 2
	sb.corner_radius_top_left = 6
	sb.corner_radius_top_right = 6
	sb.corner_radius_bottom_left = 6
	sb.corner_radius_bottom_right = 6
	sb.content_margin_left = 8
	sb.content_margin_right = 8
	sb.content_margin_top = 8
	sb.content_margin_bottom = 8
	info_panel.add_theme_stylebox_override("panel", sb)
	add_child(info_panel)
	var vb := VBoxContainer.new()
	vb.name = "VBox"
	vb.anchor_left = 0.0
	vb.anchor_top = 0.0
	vb.anchor_right = 1.0
	vb.anchor_bottom = 1.0
	vb.offset_left = 8
	vb.offset_top = 8
	vb.offset_right = -8
	vb.offset_bottom = -8
	info_panel.add_child(vb)
	# 标题
	var title := Label.new()
	title.name = "Title"
	title.text = ""
	title.add_theme_font_size_override("font_size", 16)
	title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	vb.add_child(title)
	# 描述
	var desc := Label.new()
	desc.name = "Desc"
	desc.text = ""
	desc.add_theme_font_size_override("font_size", 12)
	desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	desc.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	vb.add_child(desc)
	# 属性列表
	var props := VBoxContainer.new()
	props.name = "Props"
	vb.add_child(props)

func _hide_context_btn():
	if context_btn:
		context_btn.visible = false
		context_target = null
	if info_panel:
		info_panel.visible = false

func _show_context_btn(target: NodeItem):
	_ensure_context_btn()
	_ensure_info_panel()
	context_target = target
	context_btn.visible = true
	# 放在节点右上方
	var screen_center = get_rect().size * 0.5
	var scaled_size = background_size * zoom_level
	var background_pos = screen_center - scaled_size * 0.5 + view_offset
	var screen_pos: Vector2 = background_pos + target.position * zoom_level
	context_btn.position = screen_pos + Vector2(24, -28)
	# 信息面板在按钮下方
	info_panel.position = context_btn.position + Vector2(0, 28)
	# 重要：先清零最小高度，避免上一次打开时的高度锁定
	info_panel.custom_minimum_size = Vector2(260, 0)
	_update_info_panel(target)
	_resize_info_panel_to_content()
	info_panel.visible = true

func _fetch_basic_bullet_stats() -> Dictionary:
	var stats := {}
	# 试图从脚本读取（如果存在）
	var bullet_script_path := "res://scripts/bullet/bullet.gd"
	if ResourceLoader.exists(bullet_script_path):
		var scr: Script = load(bullet_script_path) as Script
		if scr:
			# 使用脚本自身的基类实例，避免类型不匹配（例如 bullet.gd 继承自 Area2D）
			var tmp: Object = scr.new()
			# 读取常见字段（若不存在则忽略）
			var v
			v = tmp.get("bullet_count")
			if v != null: stats["count"] = v
			v = tmp.get("damage")
			if v != null: stats["damage"] = v
			v = tmp.get("crit_rate")
			if v != null: stats["crit_chance"] = v
			v = tmp.get("crit_damage_multiplier")
			if v != null: stats["crit_multiplier"] = v
			v = tmp.get("speed")
			if v != null: stats["speed"] = v
			v = tmp.get("lifetime")
			if v != null: stats["life_time"] = v
			v = tmp.get("pierce_count")
			if v != null: stats["pierce"] = v
			v = tmp.get("spread_angle")
			if v != null: stats["spread_angle"] = v
			v = tmp.get("damage_type")
			if v != null: stats["element_type"] = String(v)
			if tmp is Node:
				(tmp as Node).queue_free()
	return stats

func _update_info_panel(target: NodeItem) -> void:
	if info_panel == null:
		return
	var vb := info_panel.get_node("VBox") as VBoxContainer
	var title := vb.get_node("Title") as Label
	var desc := vb.get_node("Desc") as Label
	var props := vb.get_node("Props") as VBoxContainer
	# 标题与描述
	title.text = target.name
	if target.node_type == "attack":
		desc.text = "基础攻击节点，发射普通子弹"
	elif target.node_type == "modifier":
		desc.text = "修饰节点，可以增强攻击节点的属性"
	else:
		desc.text = "节点描述"
	
	# 清空旧属性
	for c in props.get_children():
		c.queue_free()
	
	# 根据节点类型显示不同的属性
	if target.node_type == "attack":
		# 攻击节点显示子弹属性，包括修饰节点叠加效果
		var st := _fetch_basic_bullet_stats()
		var modifiers := _get_modifiers_for_attack_node(target)
		var modifier_bonus := _calculate_modifier_bonus(modifiers)
		
		_info_add_prop(props, "子弹数量:", (str(st["count"]) if st.has("count") else "1"), _format_bonus(modifier_bonus.get("count", 0)))
		_info_add_prop(props, "子弹伤害:", (str(st["damage"]) if st.has("damage") else "4"), _format_bonus(modifier_bonus.get("damage", 0)))
		_info_add_prop(props, "暴击概率:", ((str(int(st["crit_chance"] * 100)) + "%") if st.has("crit_chance") else "50%"), _format_bonus_percent(modifier_bonus.get("crit_chance", 0)))
		_info_add_prop(props, "暴击效果:", ((str(int(st["crit_multiplier"] * 100)) + "%") if st.has("crit_multiplier") else "150%"), _format_bonus_percent(modifier_bonus.get("crit_multiplier", 0)))
		_info_add_prop(props, "子弹速度:", (str(int(st["speed"])) if st.has("speed") else "300"), _format_bonus(modifier_bonus.get("speed", 0)))
		_info_add_prop(props, "存在时间:", (str(st["life_time"]) if st.has("life_time") else "2.0"), _format_bonus(modifier_bonus.get("life_time", 0)))
		_info_add_prop(props, "穿透数量:", (str(st["pierce"]) if st.has("pierce") else "0"), _format_bonus(modifier_bonus.get("pierce", 0)))
		_info_add_prop(props, "散射角度:", (str(st["spread_angle"]) if st.has("spread_angle") else "8.0"), _format_bonus(modifier_bonus.get("spread_angle", 0)))
		_info_add_prop(props, "伤害类型:", (String(st["element_type"]) if st.has("element_type") else "无"), "")
		
		# 显示能量消耗
		var total_energy_cost := _get_node_energy_cost(target)
		for modifier in modifiers:
			total_energy_cost += _get_node_energy_cost(modifier)
		_info_add_prop(props, "能量消耗:", str(total_energy_cost), "")
	elif target.node_type == "modifier":
		# 修饰节点只显示自己拥有的属性
		if target.id == "heavy_bullet":
			_info_add_prop(props, "攻击伤害:", "+4", "")
			_info_add_prop(props, "暴击概率:", "+20%", "")
			_info_add_prop(props, "子弹速度:", "-100", "")
			_info_add_prop(props, "子弹时间:", "+1", "")
			_info_add_prop(props, "能量消耗:", "10", "")
		else:
			_info_add_prop(props, "修饰效果:", "待定义", "")

func _info_add_prop(props: VBoxContainer, label_text: String, value_text: String, extra: String) -> void:
	var h := HBoxContainer.new()
	var l := Label.new(); l.text = label_text; l.add_theme_font_size_override("font_size", 12); h.add_child(l)
	var v := Label.new(); v.text = value_text; v.add_theme_font_size_override("font_size", 12); h.add_child(v)
	var e := Label.new(); e.text = extra; e.add_theme_font_size_override("font_size", 12); h.add_child(e)
	props.add_child(h)

# 获取指定攻击节点的所有修饰节点
func _get_modifiers_for_attack_node(attack_node: NodeItem) -> Array:
	var modifiers := []
	var attack_idx := -1
	var root_idx := -1
	
	# 找到攻击节点和根节点的索引
	for i in range(nodes.size()):
		if nodes[i] == attack_node:
			attack_idx = i
		elif nodes[i].is_root:
			root_idx = i
	
	if attack_idx == -1 or root_idx == -1:
		print("[ConfigGraph] 未找到攻击节点或根节点索引")
		return modifiers
	
	# 使用深度优先搜索找到从根节点到攻击节点路径上的所有修饰节点
	var visited := []
	var path_modifiers := []
	_dfs_find_modifiers_in_path(root_idx, attack_idx, visited, path_modifiers)
	
	modifiers = path_modifiers
	print("[ConfigGraph] 攻击节点 ", attack_node.name, " 的修饰节点数量: ", modifiers.size())
	return modifiers

# 深度优先搜索：找到从根节点到攻击节点路径上的所有修饰节点
func _dfs_find_modifiers_in_path(current_idx: int, target_idx: int, visited: Array, modifiers: Array) -> bool:
	# 如果到达目标攻击节点，返回true
	if current_idx == target_idx:
		return true
	
	# 如果已经访问过，返回false
	if current_idx in visited:
		return false
	
	# 标记为已访问
	visited.append(current_idx)
	
	# 如果当前节点是修饰节点，添加到结果中
	if current_idx >= 0 and current_idx < nodes.size() and nodes[current_idx].node_type == "modifier":
		modifiers.append(nodes[current_idx])
	
	# 遍历所有边，寻找下一个节点
	for e in edges:
		if e.size() != 2:
			continue
		var a := int(e[0])
		var b := int(e[1])
		
		var next_idx := -1
		if a == current_idx:
			next_idx = b
		elif b == current_idx:
			next_idx = a
		
		# 如果找到下一个节点，递归搜索
		if next_idx != -1 and next_idx >= 0 and next_idx < nodes.size():
			if _dfs_find_modifiers_in_path(next_idx, target_idx, visited, modifiers):
				return true
	
	# 如果当前节点是修饰节点但路径不通，从结果中移除
	if current_idx >= 0 and current_idx < nodes.size() and nodes[current_idx].node_type == "modifier":
		modifiers.erase(nodes[current_idx])
	
	return false



# 计算修饰节点的属性加成
func _calculate_modifier_bonus(modifiers: Array) -> Dictionary:
	var bonus := {}
	
	for modifier in modifiers:
		# 检查是否是重型子弹节点
		if modifier.id == "heavy_bullet":
			bonus["damage"] = bonus.get("damage", 0) + 4
			bonus["crit_chance"] = bonus.get("crit_chance", 0) + 0.2  # 20%
			bonus["speed"] = bonus.get("speed", 0) - 100
			bonus["life_time"] = bonus.get("life_time", 0) + 1
			# 能量消耗叠加（供 _get_bullet_props 使用）
			bonus["energy_cost"] = bonus.get("energy_cost", 0) + 10
	
	return bonus

# 计算总能量消耗（攻击节点 + 修饰节点）
func _calculate_total_energy_cost(attack_nodes: Array) -> int:
	var total_cost := 0
	
	for attack_idx in attack_nodes:
		var attack_node = nodes[attack_idx]
		
		# 获取攻击节点的能量消耗
		var attack_energy_cost := _get_node_energy_cost(attack_node)
		total_cost += attack_energy_cost
		
		# 获取该攻击节点的修饰节点能量消耗
		var modifiers := _get_modifiers_for_attack_node(attack_node)
		for modifier in modifiers:
			var modifier_energy_cost := _get_node_energy_cost(modifier)
			total_cost += modifier_energy_cost
	
	return total_cost

# 获取单个节点的能量消耗
func _get_node_energy_cost(node: NodeItem) -> int:
	# 从节点仓库获取能量消耗
	var repository := get_tree().get_first_node_in_group("node_repository")
	if repository and repository.has_method("get_node_by_id"):
		var node_data = repository.get_node_by_id(node.id)
		if node_data and node_data.has_method("get"):
			return node_data.get("energy_cost") if node_data.get("energy_cost") != null else 0
	return 0

# 计算从根节点到指定节点的总能量消耗
func _calculate_path_energy_cost(target_node: NodeItem) -> int:
	var total_cost := 0
	
	# 如果是根节点，返回玩家能量上限
	if target_node.node_type == "root":
		var player := get_tree().get_first_node_in_group("player")
		if player and player.has_method("get"):
			return player.get("energy_max") if player.get("energy_max") != null else 50
	
	# 找到目标节点索引
	var target_idx := -1
	var root_idx := -1
	for i in range(nodes.size()):
		if nodes[i] == target_node:
			target_idx = i
		elif nodes[i].is_root:
			root_idx = i
	
	if target_idx == -1 or root_idx == -1:
		return 0
	
	# 使用DFS找到从根节点到目标节点的路径
	var visited := []
	var path_nodes := []
	_dfs_find_path(root_idx, target_idx, visited, path_nodes)
	
	# 计算路径上所有节点的能量消耗
	for node in path_nodes:
		total_cost += _get_node_energy_cost(node)
	
	return total_cost

# DFS查找从起点到终点的路径
func _dfs_find_path(current_idx: int, target_idx: int, visited: Array, path_nodes: Array) -> bool:
	# 如果到达目标节点，返回true
	if current_idx == target_idx:
		path_nodes.append(nodes[current_idx])
		return true
	
	# 如果已经访问过，返回false
	if current_idx in visited:
		return false
	
	# 标记为已访问
	visited.append(current_idx)
	
	# 将当前节点添加到路径中
	path_nodes.append(nodes[current_idx])
	
	# 遍历所有边，寻找下一个节点
	for e in edges:
		if e.size() != 2:
			continue
		var a := int(e[0])
		var b := int(e[1])
		
		var next_idx := -1
		if a == current_idx:
			next_idx = b
		elif b == current_idx:
			next_idx = a
		
		# 如果找到下一个节点，递归搜索
		if next_idx != -1 and next_idx >= 0 and next_idx < nodes.size():
			if _dfs_find_path(next_idx, target_idx, visited, path_nodes):
				return true
	
	# 如果路径不通，从结果中移除当前节点
	path_nodes.erase(nodes[current_idx])
	return false

# 格式化加成显示
func _format_bonus(value: float) -> String:
	if value == 0:
		return "(+0)"
	elif value > 0:
		return "(+" + str(int(value)) + ")"
	else:
		return "(" + str(int(value)) + ")"

# 格式化百分比加成显示
func _format_bonus_percent(value: float) -> String:
	if value == 0:
		return "(+0%)"
	elif value > 0:
		return "(+" + str(int(value * 100)) + "%)"
	else:
		return "(" + str(int(value * 100)) + "%)"

# 应用修饰节点属性到玩家
func _apply_modifier_bonus_to_player(player: Node, bonus: Dictionary):
	# 这里需要根据玩家的实际属性系统来实现
	# 暂时使用信号或直接设置属性的方式
	if player.has_method("apply_modifier_bonus"):
		player.call("apply_modifier_bonus", bonus)
	else:
		# 备用方案：直接设置玩家属性
		_apply_bonus_directly(player, bonus)

# 直接应用加成到玩家属性
func _apply_bonus_directly(_player: Node, _bonus: Dictionary):
	# 这里需要根据玩家的实际属性来实现
	# 例如：player.damage_bonus += bonus.get("damage", 0)
	pass

# 重置玩家修饰节点属性
func _reset_player_modifier_bonus(player: Node):
	if player.has_method("reset_modifier_bonus"):
		player.call("reset_modifier_bonus")
	else:
		# 备用方案：重置玩家属性
		_reset_bonus_directly(player)

# 直接重置玩家属性
func _reset_bonus_directly(_player: Node):
	# 这里需要根据玩家的实际属性来实现
	# 例如：player.damage_bonus = 0
	pass

# 清理与指定节点相关的所有连接
func _remove_connections_for_node(node_idx: int):
	var connections_to_remove := []
	
	# 找到所有包含该节点索引的连接
	for i in range(edges.size()):
		var edge = edges[i]
		if edge.size() >= 2:
			var a := int(edge[0])
			var b := int(edge[1])
			if a == node_idx or b == node_idx:
				connections_to_remove.append(i)
	
	# 从后往前删除，避免索引变化问题
	connections_to_remove.sort()
	for i in range(connections_to_remove.size() - 1, -1, -1):
		edges.remove_at(connections_to_remove[i])
	
	print("[ConfigGraph] 清理了 ", connections_to_remove.size(), " 个连接，剩余连接数：", edges.size())

# 移除节点后更新其他节点的索引
func _update_node_indices_after_removal(removed_idx: int):
	# 更新所有连接中的节点索引
	for edge in edges:
		if edge.size() >= 2:
			var a := int(edge[0])
			var b := int(edge[1])
			
			# 如果索引大于被移除的节点索引，需要减1
			if a > removed_idx:
				edge[0] = a - 1
			if b > removed_idx:
				edge[1] = b - 1

func _on_context_return_pressed():
	if context_target == null:
		return
	if context_target.is_root:
		return
	
	# 找到要移除的节点索引
	var target_idx := -1
	for i in range(nodes.size()):
		if nodes[i] == context_target:
			target_idx = i
			break
	
	if target_idx == -1:
		return
	
	# 清理与该节点相关的所有连接
	_remove_connections_for_node(target_idx)
	
	# 添加到仓库侧栏
	_append_warehouse_entry(context_target)
	
	# 从图中移除节点
	nodes.remove_at(target_idx)
	
	# 更新其他节点的索引（因为移除节点后索引会变化）
	_update_node_indices_after_removal(target_idx)
	
	_hide_context_btn()
	queue_redraw()

func _resize_info_panel_to_content() -> void:
	if info_panel == null:
		return
	var vb := info_panel.get_node("VBox") as VBoxContainer
	# 固定内部文本换行宽度：面板宽度260减去左右边距
	var sb := info_panel.get_theme_stylebox("panel")
	var margins := 0.0
	if sb:
		margins = sb.content_margin_left + sb.content_margin_right + sb.border_width_left + sb.border_width_right
	var inner_w: float = 260.0 - margins
	inner_w = max(40.0, inner_w)
	# 让标题与描述在该宽度内计算换行
	var title := vb.get_node("Title") as Label
	var desc := vb.get_node("Desc") as Label
	if title:
		title.custom_minimum_size.x = inner_w
	if desc:
		desc.custom_minimum_size.x = inner_w
	# 触发重新计算最小尺寸
	vb.queue_sort()
	# 计算组合最小高度并加上上下内容边距
	call_deferred("_resize_info_panel_after_layout")

func _resize_info_panel_after_layout() -> void:
	if info_panel == null:
		return
	_resize_info_panel_async()

func _resize_info_panel_async() -> void:
	await get_tree().process_frame
	# 再等一帧，确保容器完成布局
	await get_tree().process_frame
	var vb := info_panel.get_node("VBox") as VBoxContainer
	var sb := info_panel.get_theme_stylebox("panel")
	var top_margin := 0.0
	var bottom_margin := 0.0
	if sb:
		top_margin = sb.content_margin_top + sb.border_width_top
		bottom_margin = sb.content_margin_bottom + sb.border_width_bottom
	# 基于最小尺寸累加，避免历史尺寸残留
	var content_h: float = 0.0
	var sep := vb.get_theme_constant("separation")
	var added := 0
	for c in vb.get_children():
		if c is Control and (c as Control).is_visible_in_tree():
			content_h += (c as Control).get_minimum_size().y
			added += 1
	if added > 1:
		content_h += float(added - 1) * float(sep)
	# 目标高度 = 上边距 + 内容高度 + 下边距
	var target_h: float = top_margin + content_h + bottom_margin
	info_panel.size = Vector2(260, max(0.0, target_h))
	# 不再锁定 custom_minimum_size，高度可随内容变小
	info_panel.custom_minimum_size = Vector2(260, 0)

func _on_next_stage_pressed() -> void:
	# 由 GameUI 执行回合切换
	var gui := get_tree().get_first_node_in_group("ui_root")
	if gui == null:
		gui = get_tree().root.find_child("UI", true, false)
	if gui == null:
		# 兜底：向上查找最近具有 go_next_stage 的父节点
		var p := self as Node
		while p != null and gui == null:
			if p.has_method("go_next_stage"):
				gui = p
				break
			p = p.get_parent()
	if gui and gui.has_method("go_next_stage"):
		gui.call("go_next_stage")
	else:
		print("[ConfigGraph] 未找到 GameUI（go_next_stage）")

func _on_refresh_shop_pressed() -> void:
	# 直接在这里刷新商店
	_refresh_shop_items()

# 刷新商店物品
func _refresh_shop_items() -> void:
	print("[ConfigGraph] 刷新商店物品")
	
	# 获取商店区域
	var cfg := get_parent() as Control
	if not cfg:
		return
	var shop := cfg.get_node_or_null("PurchaseArea") as Control
	if not shop:
		print("[ConfigGraph] 未找到商店区域")
		return
	
	# 清空现有商店物品
	for child in shop.get_children():
		if child.name == "ShopContainer" or child.name.begins_with("ShopItem_"):
			child.queue_free()
	
	# 获取节点仓库
	if not node_repository:
		print("[ConfigGraph] 节点仓库未找到")
		return
	
	# 随机选择5个节点作为商店物品（可重复）
	var all_nodes = node_repository.get_all_nodes()
	var available_nodes = all_nodes.values()
	
	if available_nodes.is_empty():
		print("[ConfigGraph] 没有可用的节点")
		return
	
	# 随机选择5个节点（允许重复）
	var shop_count = 5
	var shop_items = []
	
	for i in range(shop_count):
		var random_index = randi() % available_nodes.size()
		shop_items.append(available_nodes[random_index])
	
	# 创建水平布局容器来排列商店物品
	var shop_container := HBoxContainer.new()
	shop_container.name = "ShopContainer"
	shop_container.alignment = BoxContainer.ALIGNMENT_CENTER
	shop.add_child(shop_container)
	
	# 创建商店物品UI
	for i in range(shop_items.size()):
		var node_data = shop_items[i]
		_create_shop_item(shop_container, node_data, i)

# 创建商店物品UI
func _create_shop_item(parent: Control, node_data, index: int) -> void:
	# 创建主容器
	var item_container := VBoxContainer.new()
	item_container.name = "ShopItem_" + str(index)
	item_container.custom_minimum_size = Vector2(120, 150)
	item_container.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	parent.add_child(item_container)
	
	# 创建图标（完全仿照玩家仓库的设置）
	var icon := TextureRect.new()
	icon.texture = load(node_data.icon_path) if ResourceLoader.exists(node_data.icon_path) else null
	icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
	icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	icon.custom_minimum_size = Vector2(64, 64)
	icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
	item_container.add_child(icon)
	
	# 创建名称标签
	var name_label := Label.new()
	name_label.text = node_data.name
	name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	name_label.add_theme_font_size_override("font_size", 12)
	item_container.add_child(name_label)
	
	# 创建价格标签
	var price_label := Label.new()
	price_label.text = "价格: " + str(node_data.cost)
	price_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	price_label.add_theme_font_size_override("font_size", 10)
	item_container.add_child(price_label)
	
	# 创建购买按钮
	var buy_button := Button.new()
	buy_button.text = "购买"
	buy_button.custom_minimum_size = Vector2(80, 30)
	buy_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	buy_button.pressed.connect(_on_buy_item_pressed.bind(node_data, item_container))
	item_container.add_child(buy_button)
	
	print("[ConfigGraph] 创建商店物品：", node_data.name)

# 购买物品
func _on_buy_item_pressed(node_data, item_container) -> void:
	print("[ConfigGraph] 购买物品：", node_data.name)
	
	# 获取GameUI并调用添加节点函数
	var gui := get_tree().get_first_node_in_group("ui_root")
	if gui == null:
		gui = get_tree().root.find_child("UI", true, false)
	if gui == null:
		var p := self as Node
		while p != null and gui == null:
			if p.has_method("add_node_to_warehouse"):
				gui = p
				break
			p = p.get_parent()
	
	if gui and gui.has_method("add_node_to_warehouse"):
		var success = gui.call("add_node_to_warehouse", node_data.id)
		if success:
			print("[ConfigGraph] 成功购买并添加到仓库：", node_data.name)
			# 购买成功后移除商店物品
			item_container.queue_free()
		else:
			print("[ConfigGraph] 购买失败：", node_data.name)
	else:
		print("[ConfigGraph] 未找到 GameUI（add_node_to_warehouse）")

# 从节点仓库填充仓库物品
func _populate_warehouse_from_repository() -> void:
	if not node_repository:
		print("[ConfigGraph] 节点仓库未找到，无法填充仓库")
		return
	
	# 获取所有节点
	var all_nodes = node_repository.get_all_nodes()
	for node_data in all_nodes.values():
		# 跳过基础子弹，因为它已经在初始节点中
		if node_data.id == "basic_bullet":
			continue
		
		# 创建 NodeItem
		var ni := NodeItem.new()
		ni.id = node_data.id
		ni.name = node_data.name
		ni.node_type = node_data.node_type
		ni.is_root = false
		
		# 加载图标
		if ResourceLoader.exists(node_data.icon_path):
			ni.icon = load(node_data.icon_path)
		else:
			print("警告：找不到图标文件：", node_data.icon_path)
			continue
		
		# 添加到仓库
		_append_warehouse_entry(ni)
		print("[ConfigGraph] 添加仓库物品：", node_data.name)

func _find_connect_target(mouse_screen: Vector2, background_pos: Vector2) -> NodeItem:
	# 从前往后找最近非自身节点
	var nearest: NodeItem = null
	var nearest_dist := INF
	var direction := ""
	
	for n in nodes:
		if n == dragging_node:
			continue
		var center := background_pos + n.position * zoom_level
		var d := center.distance_to(mouse_screen)
		if d < connect_radius * zoom_level and d < nearest_dist:
			nearest = n
			nearest_dist = d
			# 计算连接方向
			var relative_pos := mouse_screen - center
			if abs(relative_pos.x) > abs(relative_pos.y):
				direction = "right" if relative_pos.x > 0 else "left"
			else:
				direction = "down" if relative_pos.y > 0 else "up"
	
	connection_direction = direction
	return nearest

func _can_connect(a: NodeItem, b: NodeItem) -> bool:
	if a == null or b == null:
		return false
	
	# 基本类型允许关系
	var types_ok := (
		(a.node_type == "root" and (b.node_type == "attack" or b.node_type == "modifier")) or
		(b.node_type == "root" and (a.node_type == "attack" or a.node_type == "modifier")) or
		(a.node_type == "modifier" and b.node_type == "modifier") or
		(a.node_type == "modifier" and b.node_type == "attack") or
		(a.node_type == "attack" and b.node_type == "modifier")
	)
	if not types_ok:
		return false
	
	# 计算当前度数
	var ia: int = nodes.find(a)
	var ib: int = nodes.find(b)
	if ia == -1 or ib == -1:
		return false
	var deg_a: int = 0
	var deg_b: int = 0
	for e in edges:
		if int(e[0]) == ia or int(e[1]) == ia:
			deg_a += 1
		if int(e[0]) == ib or int(e[1]) == ib:
			deg_b += 1
	
	# 新规则：
	# - 攻击节点后不能再连接修饰节点 => 攻击节点最多只能有1条边（终点）
	# - 修饰节点后只能连接一个修饰或攻击（可串联）=> 修饰节点度数最多为2
	# - 根节点不受此限制
	if a.node_type == "attack" and deg_a >= 1:
		return false
	if b.node_type == "attack" and deg_b >= 1:
		return false
	if a.node_type == "modifier" and deg_a >= 2:
		return false
	if b.node_type == "modifier" and deg_b >= 2:
		return false
	return true

func _make_connection(a: NodeItem, b: NodeItem) -> void:
	# 建立无向连接（去重）
	var ia := nodes.find(a)
	var ib := nodes.find(b)
	if ia == -1 or ib == -1:
		print("[ConfigGraph] 无法建立连接：节点索引无效 (", ia, ", ", ib, ")")
		return
	for e in edges:
		if (e[0] == ia and e[1] == ib) or (e[0] == ib and e[1] == ia):
			print("[ConfigGraph] 连接已存在，跳过")
			return
	edges.append([ia, ib])
	
	# 根据连接方向调整被拖拽节点的位置
	if dragging_node == a:
		_adjust_node_position(a, b, connection_direction)
	elif dragging_node == b:
		_adjust_node_position(b, a, connection_direction)
	
	print("[ConfigGraph] 建立连接：", a.name, " <-> ", b.name, " (", ia, ", ", ib, ") 方向:", connection_direction)
	_rebuild_tree_relations()
	queue_redraw()

# 在游戏开始或需要时，确保根节点与第一个攻击节点默认相连
func ensure_default_connection() -> void:
	# 检查是否已经有从根节点出发的连接
	var root_item: NodeItem = null
	for n in nodes:
		if n.node_type == "root":
			root_item = n
			break
	
	if root_item == null:
		print("[ConfigGraph] 未找到根节点")
		return
	
	# 检查根节点是否已经有任何连接
	var root_idx := nodes.find(root_item)
	var has_root_connection := false
	for e in edges:
		if e.size() == 2 and (e[0] == root_idx or e[1] == root_idx):
			has_root_connection = true
			break
	
	# 如果根节点已经有连接，不创建默认连接
	if has_root_connection:
		print("[ConfigGraph] 根节点已有连接，跳过默认连接")
		return
	
	# 只有在根节点没有任何连接时，才创建默认连接
	var attack_item: NodeItem = null
	for n in nodes:
		if n.node_type == "attack" and attack_item == null:
			attack_item = n
	
	# 如果图中没有攻击节点，从仓库中创建基础子弹节点
	if attack_item == null and node_repository:
		var basic_bullet_data = node_repository.get_node_by_id("basic_bullet")
		if basic_bullet_data:
			attack_item = NodeItem.new()
			attack_item.id = basic_bullet_data.id
			attack_item.name = basic_bullet_data.name
			attack_item.node_type = basic_bullet_data.node_type
			attack_item.is_root = false
			attack_item.position = bullet_pos
			
			# 加载图标
			if ResourceLoader.exists(basic_bullet_data.icon_path):
				attack_item.icon = load(basic_bullet_data.icon_path)
			else:
				attack_item.icon = bullet_texture  # 备用图标
			
			nodes.append(attack_item)
			print("[ConfigGraph] 从仓库创建基础子弹节点")
	
	if attack_item != null:
		_make_connection(root_item, attack_item)
		print("[ConfigGraph] 默认连接 root -> ", attack_item.name)
	else:
		print("[ConfigGraph] 未能建立默认连接（缺少攻击节点）")

# === 对外接口：由玩家根节点触发攻击 ===
func perform_attack(player: Node, base_direction: Vector2) -> bool:
	# 找到 root 节点索引
	var root_idx := -1
	for i in range(nodes.size()):
		if nodes[i].node_type == "root":
			root_idx = i
			break
	if root_idx == -1:
		return false
	
	# 使用深度优先搜索找到所有从根节点可达的攻击节点
	var reachable_attack_nodes := _find_reachable_attack_nodes(root_idx)

	# 按“从12点顺时针”排序并缓存到 _attack_order
	if reachable_attack_nodes.size() <= 0:
		return false
	var root_pos: Vector2 = nodes[root_idx].position
	var angle_pairs: Array = []  # [ {"idx": int, "angle": float} ]
	for attack_idx in reachable_attack_nodes:
		var p: Vector2 = nodes[attack_idx].position
		var v: Vector2 = p - root_pos
		# 以12点(0,-1)为起点，顺时针角度
		var theta := atan2(v.y, v.x) # 以+X轴为0，逆时针为正
		var cw := fposmod(-theta + PI * 0.5, TAU) # 12点方向为0，顺时针递增
		angle_pairs.append({"idx": attack_idx, "angle": cw})
	angle_pairs.sort_custom(func(a, b): return a["angle"] < b["angle"]) 
	_attack_order.clear()
	for pair in angle_pairs:
		_attack_order.append(int(pair["idx"]))
	# 防止游标越界
	if _attack_cursor >= _attack_order.size():
		_attack_cursor = 0
	
	# 逐发扣能量：不再预检查总能量，逐个节点判断
	
	# 本次尝试从当前游标开始，若能量不足则立即尝试下一个，最多一整轮
	var used := false
	var n := _attack_order.size()
	if n > 0:
		var tries := n
		while tries > 0:
			var attack_idx: int = int(_attack_order[_attack_cursor])
			var attack_node = nodes[attack_idx]
			var modifiers := _get_modifiers_for_attack_node(attack_node)
			var modifier_bonus := _calculate_modifier_bonus(modifiers)

			# 计算该攻击所需能量（攻击节点 + 其修饰）
			var attack_cost: int = _get_node_energy_cost(attack_node)
			for m in modifiers:
				attack_cost += _get_node_energy_cost(m)

			# 检查并扣能量；不足则立即尝试下一个
			var enough_energy := true
			if player.has_method("get") and player.get("energy") != null and player.has_method("set"):
				var cur_e = int(player.get("energy"))
				if cur_e >= attack_cost:
					player.set("energy", cur_e - attack_cost)
				else:
					enough_energy = false

			if enough_energy:
				_apply_modifier_bonus_to_player(player, modifier_bonus)
				if base_direction.length() > 0.1 and player.has_method("shoot_bullet_with_direction"):
					player.call("shoot_bullet_with_direction", base_direction.normalized())
					used = true
				elif player.has_method("shoot_bullet"):
					player.call("shoot_bullet")
					used = true
				_reset_player_modifier_bonus(player)
				# 游标前进到下一个（循环），然后退出
				_attack_cursor = (_attack_cursor + 1) % n
				break
			else:
				# 能量不足，尝试下一个
				_attack_cursor = (_attack_cursor + 1) % n
				tries -= 1

	return used

# 使用深度优先搜索找到从指定节点可达的所有攻击节点
func _find_reachable_attack_nodes(start_idx: int) -> Array:
	var visited := []
	var attack_nodes := []
	_dfs_find_attacks(start_idx, visited, attack_nodes)
	return attack_nodes

# 深度优先搜索递归函数
func _dfs_find_attacks(node_idx: int, visited: Array, attack_nodes: Array):
	if node_idx in visited:
		return
	
	visited.append(node_idx)
	
	# 如果当前节点是攻击节点，添加到结果中
	if node_idx >= 0 and node_idx < nodes.size() and nodes[node_idx].node_type == "attack":
		attack_nodes.append(node_idx)
	
	# 遍历所有边，找到与当前节点相连的其他节点
	for e in edges:
		if e.size() != 2:
			continue
		var a := int(e[0])
		var b := int(e[1])
		var next_idx := -1
		
		if a == node_idx:
			next_idx = b
		elif b == node_idx:
			next_idx = a
		
		# 递归搜索相邻节点
		if next_idx != -1 and next_idx >= 0 and next_idx < nodes.size():
			_dfs_find_attacks(next_idx, visited, attack_nodes)
